using System;
using System.Collections.Generic;
#if (!WindowsCE && !SILVERLIGHT)
using System.Configuration;
using System.Xml.Serialization;
#endif
using System.IO;
using System.Net;
using System.Reflection;
using System.Xml;
using ValidationFramework.Extensions;
using ValidationFramework.Reflection;
using System.Collections;
using System.Linq;

namespace ValidationFramework.Configuration.Xml
{
	/// <summary>
	/// Allow the addition of <see cref="Rule"/>s via xml.
	/// </summary>
	/// <example>
	/// <code source="Examples\ExampleLibraryCSharp\Configuration\ConfigurationServiceExample.cs" title="The following example shows how to programmatically add Rules via an embedded resource." lang="cs"/>
	/// <code source="Examples\ExampleLibraryCSharp\Configuration\Person.validation.xml" title="This C# example assumes there is an embedded resource, named 'ValidatableClass.validation.xml' in the current Assembly, containing the following xml." lang="xml"/>
	/// <code source="Examples\ExampleLibraryVB\Configuration\ConfigurationServiceExample.vb" title="The following example shows how to programmatically add Rules via an embedded resource." lang="vbnet"/>
	/// <code source="Examples\ExampleLibraryVB\Configuration\Person.validation.xml" title="This VB example assumes there is an embedded resource, named 'ValidatableClass.validation.xml' in the current Assembly, containing the following xml." lang="xml"/>
	/// </example>
	public class XmlConfigurationService
	{
		// --- Fields
		private IParserRepository _configReaderRepository;
		private IReflectionCache _reflectionCache;
		private TypePolicyRepository _typePolicyRepository;
		private MethodPolicyRepository _methodPolicyRepository;

		// --- Constructors
		public XmlConfigurationService(IParserRepository configReaderRepository, IReflectionCache reflectionCache, TypePolicyRepository typePolicyCache, MethodPolicyRepository methodPolicyCache)
		{
			//_ruleRepository = ruleRepository;
			_configReaderRepository = configReaderRepository;
			_reflectionCache = reflectionCache;

			_typePolicyRepository = typePolicyCache;
			_methodPolicyRepository = methodPolicyCache;
		}

		// --- Methods

		/// <summary>
		/// Add validation from a particular XML file.
		/// </summary>
		/// <param name="xmlFileInfo">The <see cref="FileInfo"/> to the XML data.</param>
		/// <exception cref="ArgumentNullException"><paramref name="xmlFileInfo"/> is null.</exception>
		/// <exception cref="FileNotFoundException">The file represented by <paramref name="xmlFileInfo"/> cannot be found.</exception>
		/// <exception cref="WebException">The remote filename cannot be resolved.-or-An error occurred while processing the request.</exception>
		/// <exception cref="DirectoryNotFoundException">Part of the filename or directory cannot be found.</exception>
		public void AddXmlFile(FileInfo xmlFileInfo)
		{
			Guard.ArgumentNotNull(xmlFileInfo, "xmlFileInfo");
			AddXmlFile(xmlFileInfo.FullName);
		}

		/// <summary>
		/// Add validation from a particular XML file.
		/// </summary>
		/// <param name="xmlFileUrl">The URL for the file containing the XML data.</param>
		/// <exception cref="ArgumentNullException"><paramref name="xmlFileUrl"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="xmlFileUrl"/> is a <see cref="string.Empty"/>.</exception>
		/// <exception cref="FileNotFoundException"><paramref name="xmlFileUrl"/> cannot be found.</exception>
		/// <exception cref="WebException">The remote filename cannot be resolved.-or-An error occurred while processing the request.</exception>
		/// <exception cref="DirectoryNotFoundException">Part of the filename or directory cannot be found.</exception>
		/// <exception cref="UriFormatException"><paramref name="xmlFileUrl"/> is not a valid URI.</exception>
		public void AddXmlFile(string xmlFileUrl)
		{
			Guard.ArgumentNotNullOrEmptyString(xmlFileUrl, "xmlFileUrl");

			using (var textReader = XmlReader.Create(xmlFileUrl))
			{
				Add(textReader);
			}
		}

		/// <summary>
		/// Add all validation definitions from a directory tree.
		/// </summary>
		/// <remarks>
		/// <para>
		/// Assume that any file named <c>*.validation.xml</c> is a validation definition document.
		/// </para>
		/// <para>
		/// This method is recursive.
		/// </para>
		/// </remarks>
		/// <param name="directoryInfo">a directory</param>
		/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is null.</exception>
		/// <exception cref="DirectoryNotFoundException">The path encapsulated in <paramref name="directoryInfo"/> is invalid, such as being on an unmapped drive.</exception>
		/// <exception cref="FileNotFoundException">A file in one of the directories cannot be found.</exception>
		/// <exception cref="WebException">The remote filename cannot be resolved.-or-An error occurred while processing the request.</exception>
		/// <exception cref="DirectoryNotFoundException">Part of the filename or directory cannot be found.</exception>
		public void AddDirectory(DirectoryInfo directoryInfo)
		{
			Guard.ArgumentNotNull(directoryInfo, "directoryInfo");

			var directoryInfos = directoryInfo.GetDirectories();
			for (var directoryIndex = 0; directoryIndex < directoryInfos.Length; directoryIndex++)
			{
				var subDirectory = directoryInfos[directoryIndex];
				AddDirectory(subDirectory);
			}

			var fileInfos = directoryInfo.GetFiles("*.validation.xml");
			for (var fileIndex = 0; fileIndex < fileInfos.Length; fileIndex++)
			{
				var validationFileInfo = fileInfos[fileIndex];
				AddXmlFile(validationFileInfo);
			}
		}

		/// <summary>
		/// Add validation definitions from a <c>string</c>
		/// </summary>
		/// <param name="xml">The <see cref="string"/> containing the XML data.</param>
		/// <exception cref="ArgumentNullException"><paramref name="xml"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="xml"/> is a <see cref="string.Empty"/>.</exception>
		/// <exception cref="XmlException"><paramref name="xml"/> is not a valid <see cref="XmlNodeType.Document"/>.</exception>
		public void AddXmlString(string xml)
		{
			Guard.ArgumentNotNullOrEmptyString(xml, "xml");
			using (var reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings(), (XmlParserContext)null))
			{
				// make a StringReader for the string passed in - the StringReader
				// inherits from TextReader.  We can use the XmlTextReader.ctor that
				// takes the TextReader to build from a string...
				Add(reader);
			}
		}

		/// <summary>
		/// Adds the validation in the <see cref="XmlTextReader"/> after validating it against the validationFramework-validationDefinition-1.5 schema.
		/// </summary>
		/// <param name="xmlReader">The XmlTextReader that contains the validation.</param>
		/// <exception cref="ArgumentNullException"><paramref name="xmlReader"/> is null.</exception>
		public void Add(XmlReader xmlReader)
		{
			Guard.ArgumentNotNull(xmlReader, "xmlReader");
			xmlReader.MoveToContent();
			var validationMappingData = ValidationMappingData.Read(xmlReader);
			var datas = validationMappingData.ClassDatas;
			for (var classDataIndex = 0; classDataIndex < datas.Count; classDataIndex++)
			{
				var classData = datas[classDataIndex];
				var classType = Type.GetType(classData.TypeName, true);
				ProcessProperties(classData.Properties, classType);
				ProcessFields(classData.Fields, classType);
				ProcessMethods(classData.Methods, classType);
			}
		}

		/// <summary>
		/// Read validation definitions from a URL.
		/// </summary>
		/// <param name="url">a URL</param>
		/// <exception cref="ArgumentNullException"><paramref name="url"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="url"/> is a <see cref="string.Empty"/>.</exception>
		/// <exception cref="FileNotFoundException"><paramref name="url"/> cannot be found.</exception>
		/// <exception cref="WebException">The remote filename cannot be resolved.-or-An error occurred while processing the request.</exception>
		/// <exception cref="DirectoryNotFoundException">Part of the filename or directory cannot be found.</exception>
		/// <exception cref="UriFormatException"><paramref name="url"/> is not a valid URI.</exception>
		public void AddXmlAtUrl(string url)
		{
			Guard.ArgumentNotNullOrEmptyString(url, "url");
			AddXmlFile(url);
		}

		/// <summary>
		/// Read validation definitions from a <see cref="Uri"/>.
		/// </summary>
		/// <param name="uri">a <see cref="Uri" /> to read the mappings from.</param>
		/// <returns>This configuration object.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="uri"/> is null.</exception>
		public void AddXmlAtUrl(Uri uri)
		{
			Guard.ArgumentNotNull(uri, "uri");
			if (uri.IsFile())
			{
				AddXmlAtUrl(uri.OriginalString);
			}
			else
			{
				AddXmlAtUrl(uri.AbsolutePath);
			}
		}
#if (!SILVERLIGHT)
		/// <summary>
		/// Read validation definitions from an <see cref="XmlDocument"/>.
		/// </summary>
		/// <param name="xmlDocument">A loaded <see cref="XmlDocument"/> that contains the validation.</param>
		/// <exception cref="ArgumentNullException"><paramref name="xmlDocument"/> is null.</exception>
		public void AddDocument(XmlDocument xmlDocument)
		{
			Guard.ArgumentNotNull(xmlDocument, "xmlDocument");
			using (var memoryStream = new MemoryStream())
			{
				xmlDocument.Save(memoryStream);
				memoryStream.Position = 0;
				AddInputStream(memoryStream);
			}
		}
#endif
		/// <summary>
		/// Add validation definition from a <see cref="Stream"/>.
		/// </summary>
		/// <param name="xmlInputStream">The stream containing XML</param>
		/// <remarks>
		/// The <see cref="Stream"/> passed in through the parameter <c>xmlInputStream</c> is not <b>guaranteed</b> to be cleaned up by this method.  It is the callers responsibility to ensure that the <c>xmlInputStream</c> is properly handled when this method completes.
		/// </remarks>
		/// <exception cref="ArgumentNullException"><paramref name="xmlInputStream"/> is null.</exception>
		public void AddInputStream(Stream xmlInputStream)
		{
			Guard.ArgumentNotNull(xmlInputStream, "xmlInputStream");
			using (var textReader = XmlReader.Create(xmlInputStream))
			{
				Add(textReader);
			}
		}

		/// <summary>
		/// Adds the validation definitions in the Resource of the <see cref="Assembly"/>.
		/// </summary>
		/// <param name="path">The path to the Resource file in the <see cref="Assembly"/></param>
		/// <param name="assembly">The <see cref="Assembly"/> that contains the Resource file.</param>
		/// <exception cref="ArgumentNullException"><paramref name="assembly"/> is null.</exception>
		/// <exception cref="ArgumentNullException"><paramref name="path"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="path"/> is a <see cref="string.Empty"/>.</exception>
		/// <exception cref="FileLoadException"><paramref name="path"/> could not be loaded.</exception>
		/// <exception cref="FileNotFoundException"><paramref name="path"/> was not found.</exception>
		/// <exception cref="BadImageFormatException"><paramref name="assembly"/> is not a valid assembly.</exception>
		public void AddResource(string path, Assembly assembly)
		{
			Guard.ArgumentNotNullOrEmptyString(path, "path");
			Guard.ArgumentNotNull(assembly, "assembly");
			using (var resourceStream = assembly.GetManifestResourceStream(path))
			{
				AddInputStream(resourceStream);
			}
		}

		/// <summary>
		/// Add validation definitions a specific <see cref="Type"/>.
		/// </summary>
		/// <param name="type">The type to map.</param>
		/// <returns>This configuration object.</returns>
		/// <remarks>
		/// <para>If <paramref name="type"/> has a full name of <c>MyNameSpace.MyClass</c> the resource named <c>MyNameSpace.MyClass.validation.xml</c>, embedded in the class' assembly, will be added.</para>
		/// <para>If the mappings and classes are defined in different assemblies or don't follow the naming convention, then this method cannot be used.</para>
		/// </remarks>
		/// <exception cref="ArgumentNullException"><paramref name="type"/> is null.</exception>
		/// <exception cref="FileLoadException">The resource for <paramref name="type"/> could not be loaded.</exception>
		/// <exception cref="FileNotFoundException">The resource for <paramref name="type"/> was not found.</exception>
		public void AddResource(Type type)
		{
			Guard.ArgumentNotNull(type, "type");
			AddResource(string.Format("{0}.validation.xml", type.FullName), type.Assembly);
		}

		/// <summary>
		/// Adds all of the Assembly's Resource files that end with "<c>.validation.xml</c>"
		/// </summary>
		/// <param name="assemblyName">The name of the <see cref="Assembly"/> to load.</param>
		/// <remarks>
		/// <para>The <see cref="Assembly"/> must be in the local bin, probing path, or GAC so that the <see cref="Assembly"/> can be loaded by name.  
		/// If these conditions are not satisfied then your code should load the <see cref="Assembly"/> and call the override <see cref="AddAssembly(Assembly)"/> instead.</para> 
		/// <para>This method is case sensitive, hence the resource files must end with lower case "<c>.validation.xml</c>".</para>
		/// </remarks>
		/// <exception cref="ArgumentNullException"><paramref name="assemblyName"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="assemblyName"/> is a <see cref="string.Empty"/>.</exception>
		/// <exception cref="FileLoadException"><paramref name="assemblyName"/> was found but could not be loaded.</exception>
		/// <exception cref="FileNotFoundException"><paramref name="assemblyName"/> is not found.</exception>
		/// <exception cref="BadImageFormatException"><paramref name="assemblyName"/> is not a valid assembly.</exception>
		public void AddAssemblyResources(string assemblyName)
		{
			Guard.ArgumentNotNullOrEmptyString(assemblyName, "assemblyName");
			AddAssemblyResources(Assembly.Load(assemblyName));
		}

		/// <summary>
		/// Adds all of the <paramref name="assembly"/> resource files that end with "<c>.validation.xml</c>".
		/// </summary>
		/// <remarks>This method is case sensitive, hence the resource files must end with lower case "<c>.validation.xml</c>".</remarks>
		/// <param name="assembly">The <see cref="Assembly"/> to load.</param>
		/// <exception cref="ArgumentNullException"><paramref name="assembly"/> is null.</exception>
		public void AddAssemblyResources(Assembly assembly)
		{
			Guard.ArgumentNotNull(assembly, "assembly");

			var resources = assembly.GetManifestResourceNames();

			for (var resourceIndex = 0; resourceIndex < resources.Length; resourceIndex++)
			{
				var fileName = resources[resourceIndex];
				if ((fileName != null) && (fileName.EndsWith(".validation.xml")))
				{
					AddResource(fileName, assembly);
				}
			}
		}


		// --- Methods | Helper

		private void ProcessMethods(List<MethodData> methods, Type classType)
		{
			if ((methods == null) || (methods.Count <= 0))
				return;

			foreach (var methodData in methods)
			{
				var methodInfo = ResolveMethodInfo(classType, methodData);
				foreach (var paramData in methodData.Parameters)
				{
					var key = new ParameterKey(new MethodKey(methodInfo.MethodHandle), paramData.Name);
					var parameter = _reflectionCache.GetCachedParameter(key);
					foreach (var ruleData in paramData.RuleDatas)
					{
						RuleContext context;
						if (ruleData.RuleScope == RuleScope.Parent)
							context = new RuleContext(typeof(MethodCall), parameter);
						else
							context = new RuleContext(Type.GetTypeFromHandle(parameter.MemberTypeHandle), parameter);
						var rule = GetRule(ruleData, context);
						_methodPolicyRepository.AddParameterRule(key, rule, ruleData.RuleScope);
					}
				}
			}
		}

		private void ProcessFields(List<FieldData> fields, Type classType)
		{
			if ((fields == null) || (fields.Count <= 0))
				return;

			foreach (var fieldData in fields)
			{
				var fieldHandle = classType.GetField(fieldData.Name).FieldHandle;
				var key = new FieldKey(fieldHandle);
				var field = _reflectionCache.GetCachedField(key);
				foreach (var ruleData in fieldData.RuleDatas)
				{
					RuleContext context;
					if (ruleData.RuleScope == RuleScope.Parent)
						context = new RuleContext(classType, field);
					else
						context = new RuleContext(Type.GetTypeFromHandle(field.MemberTypeHandle), field);

					var rule = GetRule(ruleData, context);
					_typePolicyRepository.AddFieldRule(key, rule, ruleData.RuleScope);
				}
			}
		}

		private void ProcessProperties(List<PropertyData> properties, Type classType)
		{
			if ((properties == null) || (properties.Count <= 0))
				return;

			foreach (var propertyData in properties)
			{
				var key = new PropertyKey(new TypeKey(classType.TypeHandle), propertyData.Name);
				var property = _reflectionCache.GetCachedProperty(key);
				foreach(var ruleData in propertyData.RuleDatas)
                {
					RuleContext context;
					if (ruleData.RuleScope == RuleScope.Parent)
						context = new RuleContext(classType, property);
					else
						context = new RuleContext(Type.GetTypeFromHandle(property.MemberTypeHandle), property);

					var rule = GetRule(ruleData, context);
					_typePolicyRepository.AddPropertyRule(key, rule, ruleData.RuleScope);
                }
			}			
		}

		private Type ResolveType(string typeName)
		{
			Type type;
			try
			{
				type = Type.GetType(typeName, true);
				return type;
			}
			catch (Exception e)
			{
				throw new ArgumentException(string.Format("Could not load type. Tried '{0}'.", typeName), e);
			}
		}
		private MethodInfo ResolveMethodInfo(Type classType, MethodData methodData)
		{
			if (methodData.OverloadTypes.Count == 0)
			{
				return classType.GetMethod(methodData.Name);
			}
			else
			{
				var types = new Type[methodData.OverloadTypes.Count];
				for (var overloadTypeIndex = 0; overloadTypeIndex < methodData.OverloadTypes.Count; overloadTypeIndex++)
				{
					var typeData = methodData.OverloadTypes[overloadTypeIndex];
					types[overloadTypeIndex] = Type.GetType(typeData.TypeName, true);
				}
				return classType.GetMethod(methodData.Name, types);
			}
		}

		internal Rule GetRule(RuleData ruleData, RuleContext context)
		{
			var validatorType = ResolveType(ruleData.ValidatorTypeName);
			var Addr = _configReaderRepository.GetParser(validatorType);

			if (Addr == null)
				throw new ArgumentException("ValidatorAddr not found. Tried to find one for type '{0}'".Fill(validatorType.Name));

			var ruleConfigReader = new RuleConfigReader(Addr);
			return ruleConfigReader.ReadConfig(ruleData, context);
		}
	}
}